package slave;

/**
 * @author alex
 * Questa classe sara' responsabile della vera trasmissione di dei dati tramite l'utilizzo
 * del JMF, in particolare degli classi disponibili alla creazione di una datasource e di una
 * connessione RTP/RTCP
 */

import java.io.*;
import java.sql.Timestamp;
import java.util.Observable;
import java.util.Vector;
import javax.bluetooth.L2CAPConnection;
import javax.media.*;
import javax.media.protocol.*;
//import javax.media.control.BitRateControl;
import javax.media.control.TrackControl;
import javax.media.rtp.*;
import javax.media.rtp.rtcp.*;
import javax.microedition.io.Connector;
import com.intel.bluetooth.BlueCoveImpl;

import slave.sspGUI.StreamProviderMonitor;

public class AudioBtTransmit extends Observable{

	private MediaLocator locator;
	public String url;
	public String name;
	private String btAddress;
	public Processor processor = null;
	public RTPManager rtpMgrs;
	private SessionStreamManager sessionStreamManager;
	private DataSource dataOutput = null;
	public int seconds;
	public RTPL2capAdapterSender rtpl2capAdapterSender;
	public String btPort;
	public String startTime;
	public int coding;
	DataSource ds;
	public SourceDescription[] userdesclist;
	int attempts=0;
	boolean notconnect=true;
	SendStream sendStream;
	public boolean observableOff;
	private boolean started = false;
	public boolean end;
	public String fileName;
	private StreamProviderMonitor systemMonitor;
	private Vector <String> msg = new Vector<String>();
	private boolean initProcessor = false;

	/**
	 * @param systemMonitor : Riferimento al monitor(vista) del sistema.
	 * @param locator : Posizione del file audio da trasmettere.
	 * @param btAddress : Indirizzo MAC del dispositivo bluetooth 
	 * @param sessionStreamManager : {@link SessionStreamManager}
	 * @param coding : Indica la codifica che dovrà essere utilizzata nella trasmissione del file.
	 * @param startTime : Indica l' istante temporale (secondi), per l'inizio della trasmissione.
	 * @param id : identifica lo stack bluez da utilizzare
	 */
	public AudioBtTransmit(StreamProviderMonitor systemMonitor,String locator, String btAddress,SessionStreamManager ssm,int coding, String startTime) {
		this.systemMonitor=systemMonitor;
		this.addObserver(systemMonitor);
		this.locator = new MediaLocator(locator);
		this.url= locator;
		this.fileName= locator;
		this.btAddress = btAddress;
		this.sessionStreamManager=ssm;
		this.coding= coding;
		this.startTime= startTime;		
	}

	/**Questo metodo si collegherà al client al quale chiedera' conferma per la
	 * spedizione dello stream RTP. Sono previsti dopo vari test al max 10 tentativi di connessione,
	 * al termine dei quali ci sarà l'abbandono della richiesta di servizio.
	 * Questo numero è stato deciso dopo numerose prove che hanno evidenziato,
	 * che da dispositivo a dispositivo i tentativi di connessione possono variare in maniera
	 * non deterministica ma non oltre 7-8 tentativi che indicano un host down.
	 * @return
	 * @throws IOException
	 */
	@SuppressWarnings("static-access")
	public String getReceiverConfirmation () throws IOException{
		String url = "btl2cap://"+btAddress+":"+ 4097;
		L2CAPConnection con = null;
		if(!msg.isEmpty()){
			msg.clear();
		}
		msg.add(0,"EVT");
		msg.add(1,"ABT: Request BT port to: "+btAddress);
		setChanged();  	
		notifyObservers(msg);
		while(attempts!=10 && notconnect){//Gestione delle prime connessioni
			try {
				try {
					Thread.currentThread().sleep(2000);//Perido indicato per UP di un possibile device DOWN
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				con = (L2CAPConnection) Connector.open(url);
				notconnect=false;
			} catch (IOException e) {
				if(!msg.isEmpty()){
					msg.clear();
				}
				msg.add(0,"EVT");
				msg.add(1,"ABT: Exception in Request BT port to: "+btAddress);
				setChanged();  	
				notifyObservers(msg);
				attempts++;	
				e.printStackTrace();
				System.out.println("[AudioBtTransmit]Eccezione richiesta porta a "+btAddress+" N°:"+ attempts);
			}
		}//esco dal While
		if(notconnect){
			return "NOCLIENT";
		}
		else {//Dentro ELSE
			try{
				byte buffer[] = new byte[80];	
				String [] fn = fileName.split("file:./media/");	
				name = fn[1].substring(0,5);
				if(!DatasourceUsedTimeContainer.getInstance().checkFile(name)){//Non presente
					init();	
					setInitProcessor(true);
					seconds = (int) processor.getDuration().getSeconds();
					DatasourceUsedTimeContainer.getInstance().addTime(name,seconds);
				} else seconds = DatasourceUsedTimeContainer.getInstance().getTime(name);
				/*Invio la lunghezza in secondi del file*/			 
				String duration = fn[1].substring(0,5)+","+seconds;		
				con.send( duration.getBytes() );
				while(true){
					buffer = new byte[80];
					int bytes_read = con.receive(buffer);
					btPort = new String(buffer,0,bytes_read);
					System.out.println("[AudioBtTransmit] Remote bluetooth port "+btPort+" received from "+btAddress);
					if(btPort.contains("NOSERVICE")||btPort.contains("COMPLETED")) return btPort;
					if(!BluetoothClientPortContainer.getInstance().checkPort(btPort)){
						//Se SSP non sta usando questa porta la confermo. 
						String feedback ="ACK";
						con.send(feedback.getBytes());
						BluetoothClientPortContainer.getInstance().addPort(btPort);//Aggiungo la porta a quelle utilizzate
						buffer = new byte[80];
						int bytes_ack = con.receive(buffer);
						String ack = new String(buffer,0,bytes_ack);
						if(ack.contains(feedback)){
							break;			
						}
					}
					else{//Richiedo una porta diversa per la trasmissione.
						String back = "ANOTHER";
						con.send(back.getBytes());
						System.err.println("[AudioBtTransmit] Porta N°"+btPort+" già usata");
					}
				}//Fine Secondo While.
				con.close();	
				BlueCoveImpl.releaseThreadBluetoothStack();		
			}catch(Exception e){
				return "ERROR IN PORT BT CONNECTION";
			}
		}//Fine ELSE
		return null;
	}

	/**
	 * Creo il processor necessario per determinare la durata del file che
	 * sara' trasmesso, in un primo momento, e poi per il controllo della codifica e dello stream.
	 */
	public synchronized String init() {
		end=false;
		observableOff = false;
		String result;
		// Create a processor for the specified media locator
		result = createProcessor();
		if (result != null)
			return result;
		return null;
	}

	/**
	 * Inizio la creazione del dataSource
	 */
	public synchronized String start() {
		seconds = (int) processor.getDuration().getSeconds();
		String result;
		result = createTransmitter(btAddress);
		if (result != null) {
			processor.close();
			processor = null;
			return result;
		}
		double time = Double.parseDouble(startTime);
		System.err.println("[AudioBtTransmit] Istante temporale di INIZIO TRASMISSIONE: "+time);
		processor.setMediaTime(new Time(time));
		try {
			ds.connect();
		} catch (IOException e) {
			e.printStackTrace();
		}
		processor.start();
		return null;
	}

	/**Fermo la trasmissione se attiva.
	 * @param reason : Motivazione dello stop della trasmissione
	 */
	public void stop(String reason) {
		synchronized (this) {
			try{
				if (processor != null) {
					processor.stop();
					processor.close();
					processor = null;
					ds.disconnect();
					if(started){
						rtpMgrs.removeReceiveStreamListener(sessionStreamManager);	
						rtpMgrs.removeRemoteListener(sessionStreamManager);
						rtpMgrs.removeSendStreamListener(sessionStreamManager);
						rtpMgrs.removeTargets(reason);
					}
					if(rtpMgrs!=null){
						rtpMgrs.dispose();
						System.err.println("DEBUG [AudioBtTransmit] RTPManager.dispose done");	
						{
						}
					}	
					if(reason.equals("FAILOVER") || reason.equals("COMPLETED")|| reason.equals("STOPPED")){
						BluetoothClientPortContainer.getInstance().removePort(btPort);
						this.deleteObserver(systemMonitor);				
					}
					if(!msg.isEmpty()){
						msg.clear();
					}
					msg.add(0,"EVT");
					msg.add(1,"ABT: Stream Stopped to "+btAddress+" for "+reason);
					setChanged();  	
					notifyObservers(msg);
				}
				System.err.println("DEBUG [AudioBtTransmit] Check Garbage Collection");
				System.gc();
			}
			catch(Exception e){
				e.printStackTrace();	
			}
		}
	}

	private String createProcessor() {
		if (locator == null)
			return "Locator is null";
		try {
			ds = javax.media.Manager.createDataSource(locator);
		} catch (Exception e) {
			return "Couldn't create DataSource";
		}

		try {
			processor = javax.media.Manager.createProcessor(ds);
		} catch (NoProcessorException npe) {
			return "Couldn't create processor";
		} catch (IOException ioe) {
			return "IOException creating processor";
		} 
		// Si attende per la configurazione.
		boolean result = waitForState(processor, Processor.Configured);
		if (result == false)
			return "Couldn't configure processor";
		// Ritorna le tracce del file audio
		TrackControl [] tracks = processor.getTrackControls();
		if (tracks == null || tracks.length < 1)
			return "Couldn't find tracks in processor";
		// Setto il content descriptor di uscita a RAW_RTP
		ContentDescriptor cd = new ContentDescriptor(ContentDescriptor.RAW_RTP);
		processor.setContentDescriptor(cd);
		Format supported[];
		Format chosen;
		Vector <String> codec = new Vector <String>();
		boolean atLeastOneTrack = false;

		// Program the tracks: che in questo caso sarà solo una.
		for (int i = 0; i < tracks.length; i++) {
			Format format = tracks[i].getFormat();
			System.err.println("[AudioBtTransmit]Format INPUT Track:"+ format);
			if (tracks[i].isEnabled()) {
				supported = tracks[i].getSupportedFormats();
				codec.add("coding");//update viste o rimozione del client
				/**
				 * Stampo i formati disponibili.
				 */
				for (int j = 0; j < supported.length; j++) {
					System.err.println("[AudioBtTransmit] Supportati:"+ supported[j]);
					codec.add(supported[j].toString());
				}	
				if (supported.length > 0) {
					chosen = supported[coding];
					tracks[i].setFormat(chosen);
					System.err.println("[AudioBtTransmit]Format OUTPUT Stream index N°"+coding+": "+tracks[i].setFormat(chosen));
					atLeastOneTrack = true;
				} else
					tracks[i].setEnabled(false);
			} else
				tracks[i].setEnabled(false);
		}

		if (!atLeastOneTrack)
			return "Couldn't set any of the tracks to a valid RTP format";

		result = waitForState(processor, Controller.Realized);
		if (result == false)
			return "Couldn't realize processor";
		// Restituisco il datasource 
		dataOutput = processor.getDataOutput(); 
		//Send codec
		setChanged();  
		notifyObservers(codec);
		codec.clear();
		return null;
	}

	/** Viene creato un nuovo RTPManager "COMPONENTE REALIZZATO AD HOC"che gestirà la sessione RTP.
	 * @param receiverAddress : indirizzo a cui dovrò inviare lo stream
	 * @return
	 */
	private String createTransmitter(String receiverAddress) {	
		try { 	 	
			started = true;
			rtpMgrs= RTPManager.newInstance();
			rtpl2capAdapterSender = new RTPL2capAdapterSender(receiverAddress,btPort,systemMonitor,sessionStreamManager.stackBT);
			rtpMgrs.addSendStreamListener(sessionStreamManager);
			rtpMgrs.addRemoteListener(sessionStreamManager);
			rtpMgrs.addReceiveStreamListener(sessionStreamManager);
			rtpl2capAdapterSender.addRTPAdapterSenderListener(sessionStreamManager);
			rtpMgrs.initialize(rtpl2capAdapterSender);
			System.err.println( "[AudioBtTransmit]Created RTP session over: " + receiverAddress);
			sendStream = rtpMgrs.createSendStream(dataOutput,0);	
			//String rssi = sessionStreamManager.getRssi();//Invia il valore RSSI aggiornato del Client rilevato tramite RTCP al client
			//sendStream.setSourceDescription(setSourceDescription(rssi));
			sendStream.start();	
			Timestamp timeStamp = new Timestamp(System.currentTimeMillis());
			System.err.println("[AudioBtTransmit] Start stream local time for "+btAddress+" :"+timeStamp);
			if(!msg.isEmpty()){
				msg.clear();
			}
			msg.add(0,"EVT");
			msg.add(1,"ABT: Start RTP stream to: "+btAddress);
			setChanged();  	
			notifyObservers(msg);

		} catch (Exception  e) {
			e.printStackTrace();
			return e.getMessage();
		}
		return null;
	}

	/**Metodo che permette il settaggio di campi interni ai pacchetti RTCP.
	 * @return
	 */
	public SourceDescription[] setSourceDescription(String rssi){
		userdesclist= new SourceDescription[]	                                                        {
				new SourceDescription(SourceDescription
						.SOURCE_DESC_NOTE,
						rssi,
						1,
						false)};
		return userdesclist;	
	}

	/****************************************************************
	 * Convenience methods to handle processor's state changes.
	 ****************************************************************/

	private Integer stateLock = new Integer(0);
	private boolean failed = false;
	Integer getStateLock() {
		return stateLock;
	}

	void setFailed() {
		failed = true;
	}

	private synchronized boolean waitForState(Processor p, int state) {
		p.addControllerListener(new StateListener());
		failed = false;
		if (state == Processor.Configured) {
			p.configure();
		} else if (state == Processor.Realized) {
			p.realize();
		}
		while (p.getState() < state && !failed) {
			synchronized (getStateLock()) {
				try {
					getStateLock().wait();
				} catch (InterruptedException ie) {
					return false;
				}
			}
		}

		if (failed)
			return false;
		else
			return true;
	}

	public void setInitProcessor(boolean initProcessor) {
		this.initProcessor = initProcessor;
	}

	public boolean isInitProcessor() {
		return initProcessor;
	}

	/****************************************************************
	 * Inner Classes
	 ****************************************************************/

	class StateListener implements ControllerListener {
		public void controllerUpdate(ControllerEvent ce) {
			// Se ci sono errori durante la fase di configurazione o di realize
			// il Processor sarà chiuso.
			if (ce instanceof ControllerClosedEvent)
				setFailed();
			// All controller events, send a notification
			// to the waiting thread in waitForState method. ReceiveStreamListener
			if (ce instanceof ControllerEvent) {
				synchronized (getStateLock()) {
					getStateLock().notifyAll();
				}
			}
		}
	}
}


